/*
 *******************************************************************************
 *
 * Copyright (C) 2019-2020 Dialog Semiconductor.
 * This computer program includes Confidential, Proprietary Information
 * of Dialog Semiconductor. All Rights Reserved.
 *
 *******************************************************************************
 */

package com.dialog.suotalib.scan;

import android.Manifest;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanResult;
import android.bluetooth.le.ScanSettings;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.provider.Settings;
import android.util.Log;

import com.dialog.suotalib.R;
import com.dialog.suotalib.global.SuotaLibConfig;
import com.dialog.suotalib.global.SuotaLibLog;
import com.dialog.suotalib.global.SuotaProfile;
import com.dialog.suotalib.interfaces.callbacks.ISuotaScanner;
import com.dialog.suotalib.utils.MainThread;
import com.dialog.suotalib.utils.RuntimePermissionChecker;

import java.lang.ref.WeakReference;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;

import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.OnLifecycleEvent;

import static com.dialog.suotalib.global.SuotaProfile.Constants.BLUETOOTH_PACKAGE_NAME;
import static com.dialog.suotalib.global.SuotaProfile.Constants.LOCATION_PERMISSION_CODE;
import static com.dialog.suotalib.global.SuotaProfile.Constants.REQUEST_ENABLE_BT;
import static com.dialog.suotalib.global.SuotaProfile.Constants.REQUEST_LOCATION_SERVICES;
import static com.dialog.suotalib.global.SuotaProfile.ScanFailure.BLE_NOT_SUPPORTED;
import static com.dialog.suotalib.global.SuotaProfile.ScanFailure.BLUETOOTH_NOT_ENABLED;
import static com.dialog.suotalib.global.SuotaProfile.ScanFailure.FAIL;
import static com.dialog.suotalib.global.SuotaProfile.ScanFailure.LOCATION_PERMISSION_DENIED;
import static com.dialog.suotalib.global.SuotaProfile.ScanFailure.LOCATION_SERVICES_REQUIRED;
import static com.dialog.suotalib.global.SuotaProfile.ScanFailure.RESCANNING_TOO_SOON;
import static com.dialog.suotalib.global.SuotaProfile.ScanStatus.STARTED;
import static com.dialog.suotalib.global.SuotaProfile.ScanStatus.STOPPED;
import static com.dialog.suotalib.global.SuotaProfile.Uuid.SUOTA_SERVICE_UUID;

//import android.content.IntentSender;
//import com.google.android.gms.common.api.ResolvableApiException;
//import com.google.android.gms.location.LocationRequest;
//import com.google.android.gms.location.LocationServices;
//import com.google.android.gms.location.LocationSettingsRequest;
//import com.google.android.gms.location.LocationSettingsResponse;
//import com.google.android.gms.tasks.OnFailureListener;
//import com.google.android.gms.tasks.OnSuccessListener;
//import com.google.android.gms.tasks.Task;

public class SuotaScanner implements LifecycleObserver {
    private static final String TAG = "SuotaScanner";

    public static final String SCAN_LOCATION_PERMISSION = Build.VERSION.SDK_INT < 29 ?  Manifest.permission.ACCESS_COARSE_LOCATION : Manifest.permission.ACCESS_FINE_LOCATION;

    // Max 5 interactions (start or/and stop) can take place within 30 secs due to GattService limitations
    private static final long RESCAN_TIME_WINDOW = 30000;
    private static final int MAX_INTERACTIONS = 4;

    private IScanner scanner;
    private ISuotaScanner scanCallback;
    private BluetoothAdapter bluetoothAdapter;
    private RuntimePermissionChecker runtimePermissionChecker;
    private WeakReference<AppCompatActivity> activityWeakReference;
    //private Task<LocationSettingsResponse> task;

    private boolean isScanning;
    /**
     * 是否只搜索莲藕设备
     */
    private boolean onlySUOTAUuidSearch = true;
    private boolean skipLocationCheck;
    private Boolean locationServicesRequired;
    private boolean restoredToLocationServicesStep;
    private boolean locationServicesPromptVisible;
    private boolean pendingDialog;
    private final boolean allowDialogDisplay;
    private final boolean checkPermissions;

    private long scanTimeout;
    private final Handler scanTimeoutHandler;
    private final Runnable scanTimeoutRunnable;

    private List<UUID> uuids;

    /*
     * Fields below are being used to avoid getting BtGatt.GattService: App is scanning too frequently
     * due to the limitation of Max 5 interactions (start or/and stop) can take place within 30 secs
     */
    private List<Long> scanTimeStamps;
    private int scanInteractionCounter;
    private final Handler delayRescanHandler;
    private final Runnable rescanRunnable;
    private AtomicBoolean isRescanPending;
    private Lifecycle lifecycle;

    private void registerLifecycle(Lifecycle lifecycle) {
        this.lifecycle = lifecycle;
        lifecycle.addObserver(this);
    }

    private SuotaScanner(@NonNull Builder builder) {
        registerLifecycle(builder.lifecycle);
        scanInteractionCounter = 0;
        restoredToLocationServicesStep = false;
        locationServicesPromptVisible = false;
        skipLocationCheck = builder.skipLocationCheck;
        scanTimeout = builder.scanTimeout;
        checkPermissions = builder.checkPermissions;
        allowDialogDisplay = builder.allowDialogDisplay;
        activityWeakReference = builder.activity != null ? new WeakReference<>(builder.activity) : null;

        if (checkPermissions) {
            runtimePermissionChecker = builder.runtimePermissionChecker != null ? builder.runtimePermissionChecker : new RuntimePermissionChecker(builder.activity, builder.saveInstanceState);
            runtimePermissionChecker.registerPermissionRequestCallback(LOCATION_PERMISSION_CODE, new RuntimePermissionChecker.PermissionRequestCallback() {
                @Override
                public void onPermissionRequestResult(int requestCode, String[] permissions, String[] denied) {
                    onLocationPermissionResult(denied == null);
                }
            });
        }
        scanTimeoutHandler = new Handler();
        scanTimeoutRunnable = new Runnable() {
            @Override
            public void run() {
                stopScan();
            }
        };

        // Due to rescan limitation of Gatt, if the user tries to rescan too frequently,
        // we delay the rescan until its safe again to rescan.
        scanTimeStamps = new ArrayList<>();
        isRescanPending = new AtomicBoolean();
        delayRescanHandler = new Handler();
        rescanRunnable = new Runnable() {
            @Override
            public void run() {
                isRescanPending.set(false);
                scanDevices();
            }
        };

        scanner = Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP ? getScannerApi19() : getScannerApi21();
    }

    // region Public Getters and Setters

    /**
     * Returns if there is a scan currently running.
     *
     * @return <b>true</b> if there is a scan currently running
     */
    public boolean isScanning() {
        return isScanning;
    }

    /**
     * Sets value to skipLocationCheck flag. If set to true the library will allow start scanning
     * even if the Location Services are required but disabled.
     *
     * @param skipLocationCheck skipLocationFlag
     */
    public void setSkipLocationCheck(boolean skipLocationCheck) {
        this.skipLocationCheck = skipLocationCheck;
    }

    // endregion

    // region Public Methods: Scan Overloading

    /**
     * Scans for devices advertising SUOTA_SERVICE_UUID,
     * using the set timeout through {@link com.dialog.suotalib.scan.SuotaScanner.Builder#scanTimeout timeout}
     * otherwise the {@link com.dialog.suotalib.global.SuotaLibConfig.Default#SCAN_TIMEOUT default scan timeout}.
     *
     * If {@link com.dialog.suotalib.scan.SuotaScanner.Builder#checkPermissions check permission flag} is set to <b>true</b>
     * before actually start scanning the library will check and request if required the associated permissions.
     *
     * @param scanCallback triggered on any scan status update
     */
    public void scan(@NonNull final ISuotaScanner scanCallback) {
        this.scanCallback = scanCallback;
        if (checkPermissions) {
            if (runtimePermissionChecker == null) {
                if (SuotaLibLog.SCAN_ERROR)
                    Log.e(TAG, "Suota Permission Handler is null.");
                triggerOnFailure(FAIL);
            } else if (runtimePermissionChecker.checkPermission(SCAN_LOCATION_PERMISSION, R.string.suota_location_permission_rationale, LOCATION_PERMISSION_CODE)) {
                onLocationPermissionResult(true);
            }
        } else {
            scanDevices();
        }
    }

    /**
     * Scans for devices advertising SUOTA_SERVICE_UUID, using the given timeout.
     *
     * If {@link com.dialog.suotalib.scan.SuotaScanner.Builder#checkPermissions check permission flag} is set to <b>true</b>
     * before actually start scanning the library will check and request if required the associated permissions.
     *
     * @param scanCallback triggered on any scan status update
     * @see #scan(ISuotaScanner)
     */
    public void scan(long scanTimeout, @NonNull ISuotaScanner scanCallback) {
        this.scanTimeout = scanTimeout;
        scan(scanCallback);
    }

    /**
     * Scans for devices advertising any of the given uuids,
     * using the set timeout through {@link com.dialog.suotalib.scan.SuotaScanner.Builder#scanTimeout timeOut}
     * otherwise the {@link com.dialog.suotalib.global.SuotaLibConfig.Default#SCAN_TIMEOUT default scan timeout}.
     *
     * If {@link com.dialog.suotalib.scan.SuotaScanner.Builder#checkPermissions check permission flag} is set to <b>true</b>
     * before actually start scanning the library will check and request if required the associated permissions.
     *
     * @param scanCallback triggered on any scan status update
     * @see #scan(ISuotaScanner)
     */
//    public void scan(List<UUID> uuids, @NonNull ISuotaScanner scanCallback) {
//        onlySUOTAUuidSearch = false;
//        this.uuids = uuids;
//        scan(scanCallback);
//    }

    /**
     * Scans for devices advertising any of the given uuids, using the given timeout.
     *
     * If {@link com.dialog.suotalib.scan.SuotaScanner.Builder#checkPermissions check permission flag} is set to <b>true</b>
     * before actually start scanning the library will check and request if required the associated permissions.
     *
     * @param scanCallback triggered on any scan status update
     * @see #scan(ISuotaScanner)
     */
//    public void scan(List<UUID> uuids, long scanTimeout, @NonNull ISuotaScanner scanCallback) {
//        this.scanTimeout = scanTimeout;
//        scan(uuids, scanCallback);
//    }

    // endregion

    // region Public Methods

    /**
     * Stops scan if there is a scan currently running.
     */
    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
    public void stopScan() {
        if (SuotaLibLog.SCAN_DEBUG)
            Log.d(TAG, "Stop scanning");
        if (!isScanning)
            return;

        scanner.stopScanning();
        delayRescanHandler.removeCallbacks(rescanRunnable);
        scanTimeoutHandler.removeCallbacks(scanTimeoutRunnable);
        // resets
        onlySUOTAUuidSearch = true;
        uuids = null;
    }


    public boolean onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == REQUEST_ENABLE_BT) {
            if (resultCode == Activity.RESULT_OK) {
                scanDevices();
            } else {
                triggerOnFailure(SuotaProfile.ScanFailure.BLUETOOTH_NOT_ENABLED);
            }
            return true;
        } else if (requestCode == REQUEST_LOCATION_SERVICES) {
            if (resultCode == Activity.RESULT_OK) {
                locationServicesPromptVisible = false;
            } else {
                skipLocationCheck = true;
                triggerOnFailure(SuotaProfile.ScanFailure.LOCATION_SERVICES_REQUIRED);
            }
            scanDevices();
            return true;
        }
        return false;
    }

    /**
     * Takes care anything needed to be done before the release of the SuotaScanner object.
     */
    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    public void destroy() {
        if (SuotaLibLog.SCAN_DEBUG)
            Log.d(TAG, "Destroy event got triggered thus cleaning Up");
        stopScan();
        scanCallback = null;
        activityWeakReference = null;
        if (runtimePermissionChecker != null)
            runtimePermissionChecker = null;
    }


    public void onRequestPermissionsResult(final int requestCode, final String[] permissions, final int[] grantResults) {
        List<String> permissionList = Arrays.asList(permissions);
        if (permissionList.contains(SCAN_LOCATION_PERMISSION) && runtimePermissionChecker != null)
            runtimePermissionChecker.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }

    public void saveState(Bundle outState) {
        if (runtimePermissionChecker != null)
            runtimePermissionChecker.saveState(outState);
        outState.putBoolean("SUOTA#SKIP_LOCATION", skipLocationCheck);
    }

    // endregion

    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    protected void resumed() {
        if (SuotaLibLog.SCAN_DEBUG)
            Log.d(TAG, "Resumed lifecycle event triggered");
        if (!pendingDialog)
            return;

        pendingDialog = false;
    }

    // region Private Methods: Scan Requirements Handling

    /**
     * Checks all scan requirements.
     *
     * @param context context
     * @return true on SUCCESS, when all requirements passed the check or the Location Services requirement has been skipped but the other requirements passed the check.
     *
     * Otherwise returns false when one of the following errors occurred,
     * <li> {@link com.dialog.suotalib.global.SuotaProfile.ScanFailure#BLE_NOT_SUPPORTED} if PackageManager does not have system feature BLE,
     * <li> {@link com.dialog.suotalib.global.SuotaProfile.ScanFailure#BLUETOOTH_NOT_ENABLED} if bluetooth is not enabled,
     * <li> {@link com.dialog.suotalib.global.SuotaProfile.ScanFailure#LOCATION_SERVICES_REQUIRED} if location service is required but not enabled.
     */
    private boolean checkRequirements(@NonNull Context context) {
        bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
        if (bluetoothAdapter == null || !context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
            triggerOnFailure(BLE_NOT_SUPPORTED);
            return false;
        } else if (!bluetoothAdapter.isEnabled()) {
            handleBluetoothNotEnabled();
            return false;
        } else if (!locationServicesEnabled(context)) {
            handleLocationServicesRequired();
            return false;
        }
        return true;
    }

    @SuppressLint("MissingPermission")
    private void handleBluetoothNotEnabled() {
        if (allowDialogDisplay) {
            if (activityWeakReference != null && activityWeakReference.get() != null) {
                activityWeakReference.get().startActivityForResult(new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE), REQUEST_ENABLE_BT);
            }
        }
        triggerOnFailure(BLUETOOTH_NOT_ENABLED);
    }

    /*
     * NOTE: This check may fail on some devices, returning false although location services are required.
     * e.g. Xiaomi Mi8 Lite (Android 9), Samsung S10 (Android 9)
     */
    private boolean locationServicesRequired(@NonNull Context context) {
        if (locationServicesRequired == null) {
            locationServicesRequired = true;
            try {
                Resources res = context.getPackageManager().getResourcesForApplication(BLUETOOTH_PACKAGE_NAME);
                int id = res.getIdentifier("strict_location_check", "bool", BLUETOOTH_PACKAGE_NAME);
                locationServicesRequired = res.getBoolean(id);
            } catch (PackageManager.NameNotFoundException | Resources.NotFoundException e) {
                if (SuotaLibLog.SCAN_ERROR)
                    Log.e(TAG, "Failed to read location services requirement setting", e);
            }
            if (SuotaLibLog.SCAN_DEBUG)
                Log.d(TAG, "Location services requirement setting: " + locationServicesRequired);
        }
        return locationServicesRequired;
    }

    /**
     * Check if location service is required, and if so, if it is enabled.
     *
     * @param context context
     * @return true if you can continue scanning
     */
    private boolean locationServicesEnabled(Context context) {
        if (skipLocationCheck || Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
            return true;
        if (!locationServicesRequired(context))
            return true;
        return Settings.Secure.getInt(context.getContentResolver(), Settings.Secure.LOCATION_MODE, Settings.Secure.LOCATION_MODE_OFF) != Settings.Secure.LOCATION_MODE_OFF;
    }

    private void handleLocationServicesRequired() {
        if (allowDialogDisplay)
            requestLocationServices();
        triggerOnFailure(LOCATION_SERVICES_REQUIRED);
    }

    private void requestLocationServices() {
        if (locationServicesPromptVisible)
            return;
        if (activityWeakReference == null)
            return;
        final AppCompatActivity activity = activityWeakReference.get();
        if (activity == null)
            return;


    }


    private interface EnableLocationServicesAction {
        void perform();
    }



    /**
     * 扫描蓝牙
     */
    private void scanDevices() {
        if (SuotaLibLog.SCAN_DEBUG)
            Log.d(TAG, "Start scanning");
        if (restoredToLocationServicesStep || activityWeakReference == null)
            return;

        Context context = activityWeakReference.get();
        if (context != null && checkRequirements(context) && onPrepare())
            scanner.startScanning(); //扫描
    }

    private boolean onPrepare() {
        if (SuotaLibLog.SCAN_DEBUG) {
            Log.d(TAG, scanInteractionCounter + " counter");
            Log.d(TAG, scanTimeStamps.size() + " size of timestamps");
        }
        if (isScanning)
            stopScan();
        if (scanTimeStamps.size() == 0 || canRescan()) {
            scanTimeoutHandler.postDelayed(scanTimeoutRunnable, scanTimeout);
            return true;
        }
        return false;
    }


    private boolean canRescan() {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
            return true;

        if (System.currentTimeMillis() - scanTimeStamps.get(0) <= RESCAN_TIME_WINDOW && scanInteractionCounter == MAX_INTERACTIONS) {
            postponeRescan();
            return false;
        }
        while (scanTimeStamps.size() > 0 && System.currentTimeMillis() - scanTimeStamps.get(0) > RESCAN_TIME_WINDOW) {
            scanTimeStamps.remove(0);
            scanInteractionCounter--;
        }
        return true;
    }

    private void postponeRescan() {
        if (isRescanPending.getAndSet(true)) {
            if (SuotaLibLog.SCAN_DEBUG)
                Log.d(TAG, "Rescan request too soon. Another rescan request is already pending... ");
            triggerOnFailure(RESCANNING_TOO_SOON);
            return;
        }

        delayRescanHandler.removeCallbacks(rescanRunnable);
        long timeOut = RESCAN_TIME_WINDOW - (System.currentTimeMillis() - scanTimeStamps.get(0));
        delayRescanHandler.postDelayed(rescanRunnable, timeOut);
        if (SuotaLibLog.SCAN_DEBUG)
            Log.d(TAG, "Rescan request too soon, wait for " + timeOut + " millis");
        triggerOnFailure(RESCANNING_TOO_SOON);
    }

    /**
     * 判断权限
     * @param isLocationPermissionGranted
     */
    private void onLocationPermissionResult(boolean isLocationPermissionGranted) {
        if (!isLocationPermissionGranted)
            triggerOnFailure(LOCATION_PERMISSION_DENIED);
        else
            scanDevices(); //扫描蓝牙
    }

    /**
     * 对配对号进行校验
     * @param advertisedData
     * @return
     */
    private static List<UUID> getUuidsFromAdvertisementData(byte[] advertisedData) {
        List<UUID> uuidList = new ArrayList<>();
        //.wrap将字节数组包装到缓冲区中
        //.order设置字节序
        //ByteOrder.LITTLE_ENDIAN 小字节序的 ByteOrder
        ByteBuffer buffer = ByteBuffer.wrap(advertisedData).order(ByteOrder.LITTLE_ENDIAN);
        //如果buffer中字节数大于2 进行循环
        while (buffer.remaining() > 2) {
            int length = buffer.get() & 0xff;

            if (length == 0)
                break;

            int type = buffer.get() & 0xff;
//            Log.e("ByteBuffer","ByteBuffer.type"+type);
            --length;

            switch (type) {
                case 0x02: // 16位UUID的部分列表
                case 0x03: // 16位UUID的完整列表
                    while (length >= 2 && buffer.remaining() >= 2) {
                        uuidList.add(UUID.fromString(String.format("%08x-0000-1000-8000-00805f9b34fb", buffer.getShort())));
                        length -= 2;
                    }
                    break;

                case 0x06: //128位UUID的部分列表
                case 0x07: // 128位UUID的完整列表
                    while (length >= 16 && buffer.remaining() >= 16) {
                        long lsb = buffer.getLong();   //高64位
                        long msb = buffer.getLong();   //低64位
                        uuidList.add(new UUID(msb, lsb));
                        length -= 16;
                    }
                    break;
            }

            if (length > buffer.remaining())
                break;
            buffer.position(buffer.position() + length);
        }

        return uuidList;
    }

    private void scanStarted() {
        isScanning = true;
        scanTimeStamps.add(System.currentTimeMillis());
        scanInteractionCounter++;
        triggerOnScanStatusChanged(STARTED); //设置页面状态
    }

    private void scanStopped() {
        isScanning = false;
        triggerOnScanStatusChanged(STOPPED);
    }

    // endregion

    // region Callback Wrappers

    /**
     * 设置页面状态  无用
     * @param scanStatus
     */
    private void triggerOnScanStatusChanged(final SuotaProfile.ScanStatus scanStatus) {
        MainThread.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                if (scanCallback != null)
                    scanCallback.onScanStatusChange(scanStatus);
            }
        });
    }

    private void triggerOnDeviceScan(final BluetoothDevice device, final int rssi, final byte[] scanRecord) {
        MainThread.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                if (scanCallback != null)
                    scanCallback.onDeviceScan(device, rssi, scanRecord);
            }
        });
    }

    private void triggerOnFailure(final SuotaProfile.ScanFailure failure) {
        MainThread.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                if (scanCallback != null)
                    scanCallback.onFailure(failure);
            }
        });
    }

    // endregion

    // region IScanner

    private IScanner getScannerApi19() {
        return new IScanner() {
            @SuppressLint("MissingPermission")
            @Override
            public void startScanning() {
                scanStarted();
                bluetoothAdapter.startLeScan(scanCallback19);
            }

            @SuppressLint("MissingPermission")
            @Override
            public void stopScanning() {
                scanStopped();
                bluetoothAdapter.stopLeScan(scanCallback19);
            }
        };
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    private IScanner getScannerApi21() {
        return new IScanner() {
            BluetoothLeScanner bluetoothLeScanner;
            ScanSettings scanSettings;

            @SuppressLint("MissingPermission")
            @Override
            public void startScanning() {
                scanStarted(); //设置页面状态
                if (bluetoothLeScanner == null) {
                    bluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner();
                    scanSettings = new ScanSettings.Builder()
                            .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
                            .setReportDelay(0)
                            .build();
                }
                bluetoothLeScanner.startScan(null, scanSettings, scanCallback21); //扫描蓝牙
            }

            @SuppressLint("MissingPermission")
            @Override
            public void stopScanning() {
                scanStopped();
                if (bluetoothLeScanner != null && bluetoothAdapter.isEnabled())
                    bluetoothLeScanner.stopScan(scanCallback21);
            }
        };
    }

    // endregion

    // region Scan Callbacks
    /**
     * 发现蓝牙设备监听
     */
    private BluetoothAdapter.LeScanCallback scanCallback19 = new BluetoothAdapter.LeScanCallback() {
        @Override
        public void onLeScan(final BluetoothDevice device, final int rssi, final byte[] scanRecord) {
            // Keep all devices
//            device : 识别的远程设备
//            rssi : RSSI的值作为对远程蓝牙设备的报告; 0代表没有蓝牙设备;
//            scanRecode: 远程设备提供的配对号(公告)
//            Log.e("scanRecode",scanRecord.toString());
            if (!onlySUOTAUuidSearch && (uuids == null || uuids.size() == 0)) {
                triggerOnDeviceScan(device, rssi, scanRecord);
                return;
            }
            /**
             * 对处理过的UUID列表进行循环
             */
            for (final UUID uuid : getUuidsFromAdvertisementData(scanRecord)) {
                // 仅保留莲藕设备
                //如果扫描到的设备UUID 匹配索塔UUID
                if (onlySUOTAUuidSearch && uuid.equals(SUOTA_SERVICE_UUID)) {
                    Log.e("onlySUOTAUuidSearch","true");
                    triggerOnDeviceScan(device, rssi, scanRecord);
                    break;
                }

                // 仅保留广告特定服务uuid的设备
                //如果扫描到的设备UUID
                if (!onlySUOTAUuidSearch && uuids.contains(uuid)) {
                    triggerOnDeviceScan(device, rssi, scanRecord);
                    break;
                }
            }
        }
    };
    /**
     * 蓝牙扫描回调
     */
    @RequiresApi(21)
    private ScanCallback scanCallback21 = Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP ? null : new ScanCallback() {
        /**
         * 当找到BLE广告时回调。
         * @param callbackType
         * @param result
         */
        @Override
        public void onScanResult(int callbackType, ScanResult result) {
            scanCallback19.onLeScan(result.getDevice(), result.getRssi(), result.getScanRecord() != null ? result.getScanRecord().getBytes() : new byte[0]);
        }

        /**
         * 批次结果交付时回调
         * @param results
         */
        @Override
        public void onBatchScanResults(List<ScanResult> results) {
            for (ScanResult result : results) {
                scanCallback19.onLeScan(result.getDevice(), result.getRssi(), result.getScanRecord() != null ? result.getScanRecord().getBytes() : new byte[0]);
            }
        }

        /**
         * 扫描无法开始时的回叫。
         * @param errorCode
         */
        @Override
        public void onScanFailed(int errorCode) {
            scanStopped();
        }
    };

    // endregion

    // region IScanner Interface

    private interface IScanner {
        void startScanning();
        void stopScanning();
    }

    // endregion

    // region Builder

    /*
     * Using the builder pattern, we will not let the user create an SuotaScanner object
     * if he hasn't set some fields required for the class to function correctly
     */

    /**
     * Builder pattern is being used in order to create a SuotaScanner object.
     * The user can override some of the library's configuration.
     */
    public static class Builder {
        private Lifecycle lifecycle;
        private Bundle saveInstanceState;
        private AppCompatActivity activity = null;
        private RuntimePermissionChecker runtimePermissionChecker;
        private boolean skipLocationCheck;
        private boolean checkPermissions = SuotaLibConfig.CHECK_PERMISSIONS;
        private boolean allowDialogDisplay = SuotaLibConfig.ALLOW_DIALOG_DISPLAY;
        private long scanTimeout = SuotaLibConfig.Default.SCAN_TIMEOUT;

        /**
         * Mandatory to set!
         * Set the lifecycle of the "lifecycle owner" taking care of scanning
         * this will allow the library clean up everything needed.
         *
         * @param lifecycle the lifecycle
         * @return the current builder
         */
        public Builder setLifecycle(Lifecycle lifecycle) {
            this.lifecycle = lifecycle;
            return this;
        }

        /**
         * An activity must be set if the {@link #allowDialogDisplay allow dialog display flag}
         * or the {@link #checkPermissions check permissions flag} are set to <b>true</b>.
         *
         * @param activity activity
         * @return the current builder
         */
        public Builder setActivity(AppCompatActivity activity) {
            this.activity = activity;
            return this;
        }

        /**
         * Sets saveInstanceState. This is being used optionally only if permission checking is being handle by the library.
         * If set but the library doesn't handle the permission checking the value is being ignored.
         * But if the library handles permissions then this MUST be set otherwise the app will crash.
         *
         * @param saveInstanceState saveInstanceState
         * @return the current builder
         */
        public Builder setSaveInstanceState(Bundle saveInstanceState) {
            this.saveInstanceState = saveInstanceState;
            return this;
        }

        /**
         * Overrides the {@link com.dialog.suotalib.global.SuotaLibConfig#CHECK_PERMISSIONS default check permission flag}
         * just for this SuotaScanner object.
         *
         * @param checkPermissions if <b>true</b> the library will check and request permissions needed for the scan,
         *                         otherwise the user should use the SuotaScanner only if
         *                         the required permissions are granted
         * @return the current builder
         */
        public Builder setCheckPermissions(boolean checkPermissions) {
            this.checkPermissions = checkPermissions;
            return this;
        }

        public Builder setPermissionChecker(RuntimePermissionChecker runtimePermissionChecker) {
            this.runtimePermissionChecker = runtimePermissionChecker;
            return this;
        }

        /**
         * Decides if the Location Services permission is required in order to start scanning.
         *
         * @param skipLocationCheck if set to <b>true</b>, the library will allow start scanning
         *                          even if the Location Services are disabled but required
         * @return the current builder
         */
        public Builder setSkipLocationCheck(boolean skipLocationCheck) {
            this.skipLocationCheck = skipLocationCheck;
            return this;
        }

        /**
         * Overrides the {@link com.dialog.suotalib.global.SuotaLibConfig#ALLOW_DIALOG_DISPLAY default allow dialog display flag}
         * just for this SuotaScanner object.
         *
         * @param allowDialogDisplay if <b>true</b> the library will pop a fragment dialog
         *                           if any scan requirement is missing
         * @return the current builder
         */
        public Builder setAllowDialogDisplay(boolean allowDialogDisplay) {
            this.allowDialogDisplay = allowDialogDisplay;
            return this;
        }

        /**
         * Overrides the {@link com.dialog.suotalib.global.SuotaLibConfig.Default#SCAN_TIMEOUT default scan timeout}
         * just for this SuotaScanner object.
         *
         * @param scanTimeout scanTimeout
         * @return the current builder
         */
        public Builder setScanTimeout(long scanTimeout) {
            this.scanTimeout = scanTimeout;
            return this;
        }


        public SuotaScanner build() {
            if (lifecycle == null) {
                if (SuotaLibLog.SCAN_ERROR)
                    Log.e(TAG, "Cannot create SuotaScanner without providing a Lifecycle tied to it.");
                return null;
            }
            if (checkPermissions && activity == null) {
                if (SuotaLibLog.SCAN_ERROR)
                    Log.e(TAG, "Cannot create SuotaScanner without providing Activity when checkLocationPermission is enabled.");
                return null;
            }
            if (allowDialogDisplay && activity == null) {
                if (SuotaLibLog.SCAN_ERROR)
                    Log.e(TAG, "Cannot create SuotaScanner without providing Activity when ALLOW_DIALOG_DISPLAY is enabled. Dialogs may lead to starting activity for result, thus activity / context is mandatory to use dialogs feature.");
                return null;
            }

            return new SuotaScanner(this);
        }
    }

    // endregion
}
